
package com.orhandemirel.torrent;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Selcuk Orhan DEMIREL <orhandemirel@msn.com>
 */

public class Bencoding {

    public static byte[] Encode(int integer)
    {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            bytes.write("i".getBytes("ASCII"));
            byte[] byteint = String.valueOf(integer).getBytes("ASCII");
            bytes.write(byteint);
            bytes.write("e".getBytes("ASCII"));

            return bytes.toByteArray();

        } catch (IOException ex) {
            Logger.getLogger(Bencoding.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    public static byte[] Encode(String string, String encoding)
    {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            byte[] length;
            length = String.valueOf(string.length()).getBytes("ASCII");
            bytes.write(length);
            bytes.write(":".getBytes("ASCII"));
            bytes.write(string.getBytes(encoding));
            return bytes.toByteArray();
            
        } catch (IOException ex) {
            Logger.getLogger(Bencoding.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    public static byte[] Encode(byte[] string)
    {
         try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            byte[] length;
            length = String.valueOf(string.length).getBytes("ASCII");
            bytes.write(length);
            bytes.write(":".getBytes("ASCII"));
            bytes.write(string);
            return bytes.toByteArray();

        } catch (IOException ex) {
            Logger.getLogger(Bencoding.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    public static byte[] Encode(List list, String encoding)
    {
        try{
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            bytes.write("l".getBytes("ASCII"));

            for(Object elem:list)
            {
                if(elem instanceof Integer)
                {
                    bytes.write( Encode( (Integer)elem ) );
                }
                else if(elem instanceof byte[])
                {
                    bytes.write( Encode( (byte[])elem ) );
                }
                else if(elem instanceof String)
                {
                    bytes.write( Encode( (String)elem, encoding ) );
                }
                else if(elem instanceof List)
                {
                    bytes.write( Encode( (List)elem, encoding ) );
                }
                else if(elem instanceof Map)
                {
                    bytes.write( Encode( (Map)elem, encoding ) );
                }
                else
                {
                    throw new UnsupportedOperationException("Unsupported type to convert.");
                }
            }
            bytes.write("e".getBytes("ASCII"));
            return bytes.toByteArray();
        }   catch (IOException ex) {
            Logger.getLogger(Bencoding.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;

    }

    public static byte[] Encode(Map map, String encoding)
    {
        try
        {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            bytes.write("d".getBytes("ASCII"));

            TreeMap<String,Object> orderedMap = new TreeMap<String, Object>(map);
            Set<String> keySet = orderedMap.keySet();
            for(String key:keySet)
            {
                bytes.write( Encode(key.getBytes("ASCII")) );

                Object value = orderedMap.get(key);
                 if(value instanceof Integer)
                {
                    bytes.write( Encode( (Integer) value ) );
                }
                else if(value instanceof byte[])
                {
                    bytes.write( Encode( (byte[])value ) );
                }
                else if(value instanceof String)
                {
                    bytes.write( Encode( (String)value, encoding ) );
                }
                else if(value instanceof List)
                {
                    bytes.write( Encode( (List)value, encoding ) );
                }
                else if(value instanceof Map)
                {
                    bytes.write( Encode( (Map)value, encoding ) );
                }
                else
                {
                    throw new UnsupportedOperationException("Unsupported type to encode.");
                }
            }

            bytes.write("e".getBytes("ASCII"));
            return bytes.toByteArray();
        }   catch (IOException ex) {
            Logger.getLogger(Bencoding.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     *
     * @param bstring Bencoded string
     * @return Decoded Object
     */
    public static Object Decode(String bstring)
    {
        StringBuilder sb = new StringBuilder(bstring);
        return Decoder(sb);
    }
    
    public static Object Decode(byte[] bstring) throws IOException
    {
        ByteArrayInputStream bs = new ByteArrayInputStream(bstring);
        DataInputStream ds = new DataInputStream(bs);
        return Decoder(ds);
    }

    private static Object Decoder(DataInputStream ds) throws IOException
    {
        byte readByte = ds.readByte();
        if( readByte == 'i')   // Decode an integer
        {
            ds.mark(Integer.MAX_VALUE);
            int i=0;
            while( ds.readByte() != 'e')
            {
                i++;
            }
            ds.reset();
            byte[] bytes = new byte[i];
            ds.read(bytes);
            ds.readByte();
            return Integer.valueOf(new String(bytes));
        }
        else if( Character.isDigit( readByte ) )    // Decode a String
        {
            int num = Integer.parseInt( new String( new byte[] {readByte}, "ASCII")) ;
            ds.mark(Integer.MAX_VALUE);
            byte next = ds.readByte();
            while(Character.isDigit( next ))
            {
                num = (num * 10) + Integer.parseInt( new String( new byte[] {next}, "ASCII"));
                ds.mark(Integer.MAX_VALUE);
                next = ds.readByte();
            }
            ds.reset();
            ds.readByte();  // read :
            byte[] bytes = new byte[num];
            ds.read(bytes);
            return bytes;
        }
        else if( readByte == 'l')   // Decode a list
        {
            List list = new ArrayList();

            ds.mark(Integer.MAX_VALUE);
            while( ds.readByte() != 'e')
            {
               ds.reset();
               list.add( Decoder(ds) );
               ds.mark(Integer.MAX_VALUE);
            }
            
            return list;
        }
         else if (readByte == 'd' )   // Decode a dictionary
        {

            Map map = new HashMap<String, Object>();

            ds.mark(Integer.MAX_VALUE);
            while( ds.readByte() != 'e' )
            {   ds.reset();
                String key = new String ( (byte[]) Decoder(ds), "ASCII");
                map.put( key, Decoder(ds));
                ds.mark(Integer.MAX_VALUE);
            }

            return map;
        }
        else
        {
            throw new UnsupportedOperationException("Unsupported type to decode.");
        }
    }

    @Deprecated
    private static Object Decoder(StringBuilder bstring)
    {

        if( bstring.charAt(0) == 'i')    // Decode an integer
        {
            Integer val = Integer.valueOf( bstring.substring(1, bstring.indexOf("e")) );
            bstring.delete(0, bstring.indexOf("e")+1);
            return val;
        }
        else if( Character.isDigit( bstring.charAt(0) ) )   // Decode a String
        {
            int num = Integer.parseInt( bstring.substring(0, bstring.indexOf(":")) );
            String val = bstring.substring( bstring.indexOf(":") + 1 , bstring.indexOf(":") + num + 1);
            bstring.delete(0, bstring.indexOf(":")  + num + 1);
            return val;            
        }

        else if( bstring.charAt(0) == 'l' )   // Decode a list
        {

            bstring.deleteCharAt(0);
            List list = new ArrayList();
   
            while( bstring.charAt(0) != 'e' )
            {
                list.add(  Decoder(bstring) );
            }
            bstring.deleteCharAt(0);

            return list;
        }

        else if (bstring.charAt(0) == 'd' )   // Decode a dictionary
        {

            bstring.deleteCharAt(0);
            Map map = new HashMap<String, Object>();

            while( bstring.charAt(0) != 'e' )
            {
                map.put( Decoder(bstring), Decoder(bstring));
            }
            bstring.deleteCharAt(0);

            return map;
        }
        else
        {
            throw new UnsupportedOperationException("Unsupported type to decode.");
        }
    }
    
}
